﻿using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.Administration.Claims;
using Microsoft.SharePoint.WebControls;
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.DirectoryServices;
using System.Reflection;
using System.Web.UI.WebControls;

namespace ldapcp
{
    public partial class LdapcpSettings : LayoutsPageBase
    {
        SPTrustedLoginProvider CurrentTrustedLoginProvider;
        private LDAPCPConfig _PersistedObject;
        public virtual LDAPCPConfig PersistedObject
        {
            get
            {
                SPSecurity.RunWithElevatedPrivileges(delegate ()
                {
                    if (_PersistedObject == null) _PersistedObject = LDAPCPConfig.GetFromConfigDB();
                    if (_PersistedObject == null)
                    {
                        this.Web.AllowUnsafeUpdates = true;
                        _PersistedObject = LDAPCPConfig.CreatePersistedObject();
                        this.Web.AllowUnsafeUpdates = false;
                    }
                });
                return _PersistedObject;
            }
            set { _PersistedObject = value; }
        }

        AttributeHelper IdentityClaim;
        bool AllowPersistedObjectUpdate = true;

        string TextErrorNoTrustAssociation = "LDAPCP is currently not associated with any TrustedLoginProvider. It is mandatory because it cannot create permission for a trust if it is not associated to it.<br/>Visit <a href=\"http://ldapcp.codeplex.com/\" target=\"_blank\">http://ldapcp.codeplex.com/</a> to see how to associate it.<br/>Settings on this page will not be available as long as LDAPCP will not associated to a trut.";
        string TextErrorLDAPFieldsMissing = "Some mandatory fields are missing.";
        string TextErrorTestLdapConnection = "Unable to connect to LDAP for following reason:<br/>{0}<br/>It may be expected if w3wp process of central admin has intentionally no access to LDAP server.";
        string TextConnectionSuccessful = "Connection successful.";
        string TextErrorNoIdentityClaimType = "The TrustedLoginProvider {0} is set with identity claim type \"{1}\" but it is not in the claims list of LDAPCP.<br/>Please visit LDAPCP page \"claims mapping\" in Security tab to set it and return to this page afterwards.";
        string TextUpdateAdditionalLdapFilterOk = "LDAP filter was successfully applied to all user attributes. Filter is not displayed in textbox above because it can be different for each user attribute";
        string TextErrorPersistedObjectStale = "Modification is cancelled because persisted object was modified since last load of the page. Please refresh the page and try again.";

        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
        }

        protected void Page_Load(object sender, EventArgs e)
        {
            ViewState["IsDefaultADConnectionCreated"] = false;
            ViewState["ForceCheckCustomLdapConnection"] = false;

            if (!this.IsPostBack)
            {
                FileVersionInfo fvi = FileVersionInfo.GetVersionInfo(Assembly.GetAssembly(typeof(LDAPCP)).Location);
                LblTitle.Text = String.Format("LDAPCP v{0} - <a href=\"http://ldapcp.codeplex.com\" target=\"_blank\">ldapcp.codeplex.com</a>", fvi.FileVersion);
            }

            // Get trust currently associated with LDAPCP, if any
            CurrentTrustedLoginProvider = LDAPCP.GetSPTrustAssociatedWithCP(LDAPCP._ProviderInternalName);
            if (null == CurrentTrustedLoginProvider)
            {
                // Claim provider is currently not associated with any trust.
                // Display a message in the page and disable controls
                this.LabelErrorMessage.Text = TextErrorNoTrustAssociation;
                this.BtnOK.Enabled = this.BtnOKTop.Enabled = this.BtnAddLdapConnection.Enabled = false;
                this.AllowPersistedObjectUpdate = false;
                return;
            }

            //SPSecurity.RunWithElevatedPrivileges(delegate ()
            //{
            //    // Get SPPersisted Object and create it if it doesn't exist
            //    PersistedObject = LDAPCPConfig.GetFromConfigDB();
            //    if (PersistedObject == null)
            //    {
            //        this.Web.AllowUnsafeUpdates = true;
            //        PersistedObject = LDAPCPConfig.CreatePersistedObject();
            //        this.Web.AllowUnsafeUpdates = false;
            //    }
            //});

            this.IdentityClaim = PersistedObject.AttributesListProp.Find(x => String.Equals(CurrentTrustedLoginProvider.IdentityClaimTypeInformation.MappedClaimType, x.ClaimType, StringComparison.InvariantCultureIgnoreCase) && !x.CreateAsIdentityClaim);
            if (null == this.IdentityClaim)
            {
                // Identity claim type is missing in the attributes list
                this.LabelErrorMessage.Text = String.Format(this.TextErrorNoIdentityClaimType, CurrentTrustedLoginProvider.DisplayName, CurrentTrustedLoginProvider.IdentityClaimTypeInformation.MappedClaimType);
                this.BtnOK.Enabled = this.BtnOKTop.Enabled = false;
                return;
            }

            if (ViewState["PersistedObjectVersion"] == null)
                ViewState.Add("PersistedObjectVersion", PersistedObject.Version);
            if ((long)ViewState["PersistedObjectVersion"] != PersistedObject.Version)
            {
                // PersistedObject changed since last time. Should not allow any update
                this.LabelErrorMessage.Text = TextErrorPersistedObjectStale;
                this.AllowPersistedObjectUpdate = false;
                return;
            }

            if (!this.IsPostBack)
            {
                PopulateFields();
            }

            if (ViewState["LDAPpwd"] != null)
            {
                ViewState["LDAPpwd"] = TxtLdapPassword.Text;
                TxtLdapPassword.Attributes.Add("value", ViewState["LDAPpwd"].ToString());
            }
            else
            {
                ViewState["LDAPpwd"] = TxtLdapPassword.Text;
            }
        }

        void PopulateLdapConnectionGrid()
        {
            if (PersistedObject.LDAPConnectionsProp != null)
            {
                PropertyCollectionBinder pcb = new PropertyCollectionBinder();
                foreach (LDAPConnection coco in PersistedObject.LDAPConnectionsProp)
                {
                    if (coco.UserServerDirectoryEntry)
                    {
                        ViewState["IsDefaultADConnectionCreated"] = true;

                        pcb.AddRow(coco.Id, "Connect to SharePoint domain", "Process account");
                    }
                    else
                    {
                        pcb.AddRow(coco.Id, coco.Path, coco.Username);
                    }
                }
                pcb.BindGrid(grdLDAPConnections);
            }
        }

        void PopulateCblAuthenticationTypes()
        {
            Dictionary<int, string> authenticationTypesDS = EnumToList(typeof(AuthenticationTypes));
            foreach (KeyValuePair<int, string> authNType in authenticationTypesDS)
            {
                ListItem authNTypeItem = new ListItem();
                authNTypeItem.Text = authNType.Value;
                authNTypeItem.Value = authNType.Key.ToString();
                CblAuthenticationTypes.Items.Add(authNTypeItem);
            }
        }

        private void PopulateFields()
        {
            PopulateLdapConnectionGrid();
            PopulateCblAuthenticationTypes();

            this.ChkIdentityShowAdditionalAttribute.Checked = PersistedObject.DisplayLdapMatchForIdentityClaimTypeProp;
            if (String.IsNullOrEmpty(IdentityClaim.LDAPAttributeToDisplayProp))
            {
                this.RbIdentityDefault.Checked = true;
            }
            else
            {
                this.RbIdentityCustomLDAP.Checked = true;
                this.TxtLdapAttributeToDisplay.Text = IdentityClaim.LDAPAttributeToDisplayProp;
            }

            this.ChkAlwaysResolveUserInput.Checked = PersistedObject.AlwaysResolveUserInputProp;
            this.ChkFilterEnabledUsersOnly.Checked = PersistedObject.FilterEnabledUsersOnlyProp;
            this.ChkFilterSecurityGroupsOnly.Checked = PersistedObject.FilterSecurityGroupsOnlyProp;
            this.ChkFilterExactMatchOnly.Checked = PersistedObject.FilterExactMatchOnlyProp;
            this.txtTimeout.Text = PersistedObject.TimeoutProp.ToString();

            // Deprecated options that are not shown anymore in LDAPCP configuration page
            //this.ChkAddWildcardInFront.Checked = PersistedObject.AddWildcardInFrontOfQueryProp;
            //this.TxtPickerEntityGroupName.Text = PersistedObject.PickerEntityGroupNameProp;
        }

        protected void grdLDAPConnections_RowDeleting(object sender, GridViewDeleteEventArgs e)
        {
            if (!this.AllowPersistedObjectUpdate) return;
            if (PersistedObject.LDAPConnectionsProp == null) return;
            GridViewRow rowToDelete = grdLDAPConnections.Rows[e.RowIndex];

            Guid Id = new Guid(rowToDelete.Cells[0].Text);
            PersistedObject.LDAPConnectionsProp.Remove(PersistedObject.LDAPConnectionsProp.Find(x => x.Id == Id));

            // Update object in database
            UpdatePersistedObject();
            LdapcpLogging.Log(
                    String.Format("Removed a LDAP connection in PersistedObject {0}", Constants.LDAPCPCONFIG_NAME),
                    TraceSeverity.Medium,
                    EventSeverity.Information,
                    LdapcpLogging.Categories.Configuration);

            PopulateLdapConnectionGrid();
        }

        /// <summary>
        /// Update global configuration of LDAPCP, except LDAP connections
        /// </summary>
        protected void UpdateTrustConfiguration()
        {
            if (!this.AllowPersistedObjectUpdate) return;
            if (null == PersistedObject)
            {
                LdapcpLogging.Log(
                    String.Format("PersistedObject {0} should not be null.", Constants.LDAPCPCONFIG_NAME),
                    TraceSeverity.Unexpected,
                    EventSeverity.Error,
                    LdapcpLogging.Categories.Configuration);
                return;
            }

            if (null == CurrentTrustedLoginProvider)
            {
                LdapcpLogging.Log(
                    "Trust associated with LDAPCP could not be found.",
                    TraceSeverity.Unexpected,
                    EventSeverity.Error,
                    LdapcpLogging.Categories.Configuration);
                return;
            }

            // Handle identity claim type
            PersistedObject.DisplayLdapMatchForIdentityClaimTypeProp = this.ChkIdentityShowAdditionalAttribute.Checked;
            if (this.RbIdentityCustomLDAP.Checked)
            {
                IdentityClaim.LDAPAttributeToDisplayProp = this.TxtLdapAttributeToDisplay.Text;
            }
            else
            {
                IdentityClaim.LDAPAttributeToDisplayProp = String.Empty;
            }

            PersistedObject.AlwaysResolveUserInputProp = this.ChkAlwaysResolveUserInput.Checked;
            PersistedObject.FilterEnabledUsersOnlyProp = this.ChkFilterEnabledUsersOnly.Checked;
            PersistedObject.FilterSecurityGroupsOnlyProp = this.ChkFilterSecurityGroupsOnly.Checked;
            PersistedObject.FilterExactMatchOnlyProp = this.ChkFilterExactMatchOnly.Checked;

            int timeOut;
            if (!Int32.TryParse(this.txtTimeout.Text, out timeOut) || timeOut < 0)
                timeOut = Constants.LDAPCPCONFIG_TIMEOUT; //set to default if unable to parse

            PersistedObject.TimeoutProp = timeOut;

            // Deprecated options that are not shown anymore in LDAPCP configuration page
            //PersistedObject.AddWildcardInFrontOfQuery = this.ChkAddWildcardInFront.Checked;
            //PersistedObject.PickerEntityGroupName = this.TxtPickerEntityGroupName.Text;

            UpdatePersistedObject();
            LdapcpLogging.Log(
                String.Format("Updated PersistedObject {0}", Constants.LDAPCPCONFIG_NAME),
                TraceSeverity.Medium,
                EventSeverity.Information,
                LdapcpLogging.Categories.Configuration);
        }

        /// <summary>
        /// Commit changes made to persisted object in configuration database
        /// </summary>
        void UpdatePersistedObject()
        {
            // Update object in database
            SPSecurity.RunWithElevatedPrivileges(delegate ()
            {
                this.Web.AllowUnsafeUpdates = true;
                PersistedObject.Update();
                this.Web.AllowUnsafeUpdates = false;
            });
            ViewState["PersistedObjectVersion"] = PersistedObject.Version;
        }

        protected void ValidateLdapConnection()
        {
            ViewState["ForceCheckCustomLdapConnection"] = true;
            if (this.TxtLdapConnectionString.Text == String.Empty || this.TxtLdapPassword.Text == String.Empty || this.TxtLdapUsername.Text == String.Empty)
            {
                this.LabelErrorTestLdapConnection.Text = TextErrorLDAPFieldsMissing;
                return;
            }

            DirectoryEntry de = null;
            DirectorySearcher deSearch = new DirectorySearcher();
            try
            {
                AuthenticationTypes authNTypes = GetSelectedAuthenticationTypes(false);
                de = new DirectoryEntry(this.TxtLdapConnectionString.Text, this.TxtLdapUsername.Text, this.TxtLdapPassword.Text, authNTypes);
                deSearch.SearchRoot = de;
                deSearch.FindOne();
                this.LabelTestLdapConnectionOK.Text = TextConnectionSuccessful;
            }
            catch (Exception ex)
            {
                LdapcpLogging.LogException(LDAPCP._ProviderInternalName, "while testing LDAP connection", LdapcpLogging.Categories.Configuration, ex);
                this.LabelErrorTestLdapConnection.Text = String.Format(TextErrorTestLdapConnection, ex.Message);
            }
            finally
            {
                if (deSearch != null) deSearch.Dispose();
                if (de != null) de.Dispose();
            }

            // Required to set radio buttons of LDAP connections correctly in UI
            PopulateLdapConnectionGrid();
        }

        /// <summary>
        /// Parse checkbox list CblAuthenticationTypes to find authentication modes selected
        /// </summary>
        /// <returns></returns>
        AuthenticationTypes GetSelectedAuthenticationTypes(bool ClearSelection)
        {
            AuthenticationTypes authNTypes = 0;
            foreach (ListItem item in this.CblAuthenticationTypes.Items)
            {
                if (!item.Selected) continue;
                int selectedType;
                if (!Int32.TryParse(item.Value, out selectedType)) continue;
                authNTypes += selectedType;
                if (ClearSelection) item.Selected = false;
            }
            return authNTypes;
        }

        protected void BtnOK_Click(Object sender, EventArgs e)
        {
            this.UpdateTrustConfiguration();
            Response.Redirect("/Security.aspx", false);
        }

        protected void BtnResetLDAPCPConfig_Click(Object sender, EventArgs e)
        {
            LDAPCPConfig.DeleteLDAPCPConfig();
            Response.Redirect(Request.RawUrl, false);
        }

        protected void BtnUpdateAdditionalUserLdapFilter_Click(Object sender, EventArgs e)
        {
            UpdateAdditionalUserLdapFilter();
        }

        void UpdateAdditionalUserLdapFilter()
        {
            if (!this.AllowPersistedObjectUpdate) return;
            foreach (var userAttr in this.PersistedObject.AttributesListProp.FindAll(x => x.ClaimEntityType == SPClaimEntityTypes.User || x.CreateAsIdentityClaim))
            {
                userAttr.AdditionalLDAPFilterProp = this.TxtAdditionalUserLdapFilter.Text;
            }
            this.UpdatePersistedObject();
            LabelUpdateAdditionalLdapFilterOk.Text = this.TextUpdateAdditionalLdapFilterOk;
        }

        protected void BtnTestLdapConnection_Click(Object sender, EventArgs e)
        {
            this.ValidateLdapConnection();
        }

        private TableCell GetTableCell(string Value)
        {
            TableCell tc = new TableCell();
            tc.Text = Value;
            return tc;
        }

        protected void BtnAddLdapConnection_Click(object sender, EventArgs e)
        {
            AddLdapConnection();
        }

        /// <summary>
        /// Add new LDAP connection to collection in persisted object
        /// </summary>
        void AddLdapConnection()
        {
            if (!this.AllowPersistedObjectUpdate) return;
            if (null == PersistedObject)
            {
                LdapcpLogging.Log(
                    String.Format("PersistedObject {0} should not be null.", Constants.LDAPCPCONFIG_NAME),
                    TraceSeverity.Unexpected,
                    EventSeverity.Error,
                    LdapcpLogging.Categories.Configuration);
                return;
            }

            if (null == CurrentTrustedLoginProvider)
            {
                LdapcpLogging.Log(
                    "Trust associated with LDAPCP could not be found.",
                    TraceSeverity.Unexpected,
                    EventSeverity.Error,
                    LdapcpLogging.Categories.Configuration);
                return;
            }

            if (this.RbUseCustomConnection.Checked && (this.TxtLdapConnectionString.Text == String.Empty || this.TxtLdapUsername.Text == String.Empty || this.TxtLdapPassword.Text == String.Empty))
            {
                this.LabelErrorTestLdapConnection.Text = TextErrorLDAPFieldsMissing;
                return;
            }

            if (this.RbUseServerDomain.Checked)
            {
                PersistedObject.LDAPConnectionsProp.Add(new LDAPConnection { UserServerDirectoryEntry = true });
            }
            else
            {
                AuthenticationTypes authNType = GetSelectedAuthenticationTypes(true);
                PersistedObject.LDAPConnectionsProp.Add(
                    new LDAPConnection
                    {
                        UserServerDirectoryEntry = false,
                        Path = this.TxtLdapConnectionString.Text,
                        Username = this.TxtLdapUsername.Text,
                        Password = this.TxtLdapPassword.Text,
                        AuthenticationTypes = authNType,
                    }
                );
            }

            // Update object in database
            UpdatePersistedObject();
            LdapcpLogging.Log(
                   String.Format("Added a new LDAP connection in PersistedObject {0}", Constants.LDAPCPCONFIG_NAME),
                   TraceSeverity.Medium,
                   EventSeverity.Information,
                   LdapcpLogging.Categories.Configuration);

            PopulateLdapConnectionGrid();
            ViewState["LDAPpwd"] = String.Empty;
            TxtLdapPassword.Attributes.Remove("value");
            this.TxtLdapUsername.Text = this.TxtLdapPassword.Text = String.Empty;
            this.TxtLdapConnectionString.Text = "LDAP://";
        }

        public static Dictionary<int, string> EnumToList(Type t)
        {
            Dictionary<int, string> list = new Dictionary<int, string>();
            foreach (var v in Enum.GetValues(t))
            {
                string name = Enum.GetName(t, (int)v);
                // Encryption and SecureSocketsLayer have same value and it will violate uniqueness of key if attempt to add both to Dictionary
                if (String.Equals(name, "Encryption", StringComparison.InvariantCultureIgnoreCase) && list.ContainsValue("Encryption")) continue;
                list.Add((int)v, name);
            }
            return list;
        }
    }

    public class PropertyCollectionBinder
    {
        protected DataTable PropertyCollection = new DataTable();
        public PropertyCollectionBinder()
        {
            PropertyCollection.Columns.Add("Id", typeof(Guid));
            PropertyCollection.Columns.Add("Path", typeof(string));
            PropertyCollection.Columns.Add("Username", typeof(string));
        }

        public void AddRow(Guid Id, string Path, string Username)
        {
            DataRow newRow = PropertyCollection.Rows.Add();
            newRow["Id"] = Id;
            newRow["Path"] = Path;
            newRow["Username"] = Username;
        }

        public void BindGrid(SPGridView grid)
        {
            grid.DataSource = PropertyCollection.DefaultView;
            grid.DataBind();
        }
    }
}
